feat(installer): Avalonia GUI wizard, CLI, and Velopack packaging - #59
Merged
Conversation
Plan to replace the three diverged installer scripts (install-linux.sh, install-windows.ps1, install-macos.sh) with one C# .NET 10 codebase: Optimum.Bootstrap.Core (library), Optimum.Cli (NDJSON front end for RiftLauncher), and Optimum.Installer (Avalonia GUI). Records the engine contract, the cross-OS gap resolutions, the phased rollout, and the open questions. No code yet.
EULA text gets rewritten to match LICENSE-SCOPE.md, consent posture B (notice, no hard gate), one legal question still out. macOS distribution deferred: no Apple Developer account yet, so no signed macOS release; install-macos.sh stays. macOS packaging when it ships is a Velopack .pkg, not Parcel. Velopack on .NET 10 confirmed by a local spike for linux-x64 and win-x64 including a delta.
The legal question resolved: local decompilation of the user's own game needs explicit consent. GUI gates on a checkbox, Optimum.Cli build requires --acknowledge-decompile (refused as bad-input if absent), RiftLauncher renders the text and passes the flag. Updates the engine contract, the CI steps, Phase 2, and Phase 7.
Six projects under a new /Installer/ folder in VintageStory.slnx, built and tested bootstrap-free via Optimum.Installer.slnf: - Optimum.Bootstrap.Core: ProgressPhase and FailureReason contract types, CoreInfo.Version. CliWrap referenced for the Phase 2 build driver. - Optimum.Cli (AssemblyName 'optimum'): answers --version, exits 2 with usage on anything else. CliRunner split out for testing. - Optimum.Installer: one-window Avalonia 12.1 app, CommunityToolkit.Mvvm, compiled bindings. - Three test projects, 11 tests, one a headless Avalonia render. Installer tests are on xUnit v3 (Avalonia.Headless.XUnit 12.x requires it); the other two stay on v2 like the existing test projects. ci-installer.yml runs the tests and a velopack-smoke job (packs two versions of Optimum.Cli, asserts the delta builds) on push and PR touching installer paths. INSTALLER-PLAN.md section 11 Phase 0 marked done.
…ase 1) Optimum.Bootstrap.Core gains its detection and protocol layer behind an ISystemProbe seam so tests run against an in-memory host: - Prerequisites: PrerequisiteScanner over the check-prereqs.sh tool list, DotnetSdkProbe (ports check_dotnet10), ConfigFiles (reads the ilspycmd pin and range from .config/), IlspycmdVersion range check, NixEnvironment (NixOS and non-FHS detection and refusal). pwsh is modelled as required-for-packaging, not required, so a Linux tar.gz build is not blocked by it. - Acquisition: SdkAcquisition plans the dotnet-install invocation with --jsonfile global.json and refuses on NixOS or a non-FHS host; IlspycmdAcquisition emits the exact 'tool update' arguments the shell test asserts. - Paths: InstallPathGuard consolidates guard_install_dir and Assert-SafeInstallerPaths plus a symlink-component walk; every section 9 case is covered. - DataPathProbe: session-aware data-folder detection, ported from prompt_data_path and widened per platform. - NdjsonWriter: the section 4 stream, with monotonic 0-99 progress and a single terminal result enforced. - Licensing: consent-notice.md rewritten to match LICENSE-SCOPE.md; ConsentNotice.AcknowledgeFlag is the CLI's --acknowledge-decompile. 72 Core tests, including the ported install-linux-prerequisites.sh and install-linux-nixos.sh behaviors. Full suite 76 green.
The synchronous ReadToEnd pair in SystemProbe.Run could deadlock a probe whose child filled one pipe buffer while the reader blocked on the other.
An adversarial pass against the shell sources found three divergences worth closing: - command -v equivalent now checks the execute bit and continues past a non-executable file of the right name, matching the shell. Added ISystemProbe.IsExecutable; DotnetSdkProbe gates on it. - InstallPathGuard rejects a symlinked install or data directory itself, not any symlinked ancestor. A home or mount point that is a symlink is normal and was being refused. The full assertNoSymlinkComponents walk stays in SymlinkComponentCheck for paths with a trusted base. - NdjsonWriter emits a warn log and counts it when it adjusts a caller's progress value, so an engine-side miscalculation is not silent. Also: HasNet10Line anchors at column 0 like the shell grep, and the ilspycmd version token is trimmed. 79 tests green.
…ase 2) Core build layer: - ScriptBuildDriver drives scripts/bootstrap.*, dotnet build VintageStory.slnx, and the platform packaging script through CliWrap, one ProgressPhase per step, with per-step FailureReason mapping. BootstrapFailureClassifier splits a failed bootstrap into patch-conflict vs decompile-failed. - Capabilities reads forks.json and the patches-*-bridge dirs; PackageLayout, InstallManifest, PackageDeployer (guard + copy + manifest, non-transactional; the stage/backup/rollback is Phase 4), Uninstaller (manifest-driven), and a header-only RuntimeValidator. Optimum.Cli: preflight, build, install, validate, uninstall, capabilities, --version. build requires --acknowledge-decompile. --json emits the section 4 NDJSON stream via NdjsonWriter; plain text otherwise. SIGTERM/SIGINT cancel a build to a cancelled result. EngineOutput bridges the two modes. Tests: 12 CLI tests including the NDJSON contract check (NdjsonStream, twin of scripts/check-ndjson-stream.py), patch-conflict and cancelled reasons, the consent gate, and path-absoluteness. 95 Core tests (deploy/uninstall round trips on real temp dirs). CI: ci-installer.yml gains a cli-contract job; ci-platform-bootstrap.yml's linux job runs Optimum.Cli build end to end plus validate, timeout raised to 60m. 109 tests green.
Adversarial review of the CLI and build driver surfaced: - macOS build always failed: LocatePackage only looked for an Optimum-v* directory, but package-macos.sh produces Optimum.app. Now per-platform. - SIGTERM cleanup deleted the whole --output tree. The output guard now refuses any non-empty directory (files or subdirs), and cancellation removes only what the build wrote, keeping a pre-existing empty dir. - --client-archive was not forwarded to the packaging step, so it re-downloaded ~500 MB. Now forwarded to bootstrap and packaging both. - Windows build injected -ClientArchive __skip__ when no archive was given, which makes bootstrap.ps1 throw. Now the flag is omitted so bootstrap.ps1 downloads. - check-patches.sh --strict-unavailable now runs between build and package, matching CI; a header-level RuntimeValidator runs on the produced package. - Uninstaller confines manifest entries to the install directory; a rooted or ../ entry is refused, not followed. - install refuses a non-empty target rather than deleting it. In-place replace with rollback stays Phase 4. - Manifest version comes from the Optimum-v<version> package name. - CopyDirectory uses Path.GetRelativePath, not string replace. - --repo-root documented in the contract; query verbs (preflight, capabilities) documented as single-document, not stream. - Cancellation test now drives a real cancelled token through CliRunner. 113 tests green.
Optimum.Installer is an Avalonia 12 MVVM app that drives Core in-process. - MainWindowViewModel: the wizard shell and state machine (Prerequisites, Options, a mandatory EULA modal, Progress, Completion). Backward nav only Options -> Prerequisites, blocked once Progress starts. Screen view models raise the transition they want; the shell decides. - PrerequisitesViewModel over PrerequisiteScanner, gates Continue on BlocksBuild; reports repo-root-missing when run outside a checkout. - OptionsViewModel: per-platform default install dir, InstallPathGuard on every change into an inline error, DataPathProbe pickup, a version selector only when Capabilities reports a bridge set. - EulaViewModel: accept gated on scroll-to-end plus a checkbox. - ProgressViewModel: its own IBuildObserver; runs ScriptBuildDriver then PackageDeployer under one bar with an honest time estimate; filters raw subprocess lines through InstallerLogFilter (ported from install-windows.ps1). - CompletionViewModel: Launch, Try again, View log. - ViewLocator resolves view models to views; five screen views plus the EULA view, minimal Fluent styling. FakeSystemProbe moved to a plain Optimum.Bootstrap.Core.TestSupport project so the xUnit v2 and v3 test projects can share it without the v2/v3 FactAttribute collision. Optimum.Installer.Tests: 27 tests (25 plain xUnit v3 view-model tests, two Avalonia.Headless render tests), no xvfb. 138 tests green overall.
Adversarial review of the wizard surfaced: - Launch button opened the .sh launcher in an editor via xdg-open on Linux. CompletionViewModel now runs it directly (cmd /c on Windows), with the install dir as the working directory. - The multi-hundred-MB temp build tree was never cleaned. ProgressViewModel owns it now and deletes it in a finally after the deploy. - Elapsed time and ETA froze between driver progress lines. A 1s timer ticks the clock independently; the ETA is labelled rough. - Cancel was a straight kill. IBuildDriver.RunAsync takes a graceful and a forceful token; ProgressViewModel.Cancel signals graceful, then forces after 10s. CliWrap's two-token ListenAsync overload drives it. - No re-entrancy guard: a double-click on Accept could race two builds. MainWindowViewModel guards with _installStarted. - Retry kept a stale Options screen. RestartFromPrerequisites rebuilds it so DataPathProbe and Capabilities re-run. - A cancelled run had no retry route. CanRetry covers cancelled too. - The EULA scroll gate could latch true during layout (Extent 0) or never fire for short content. The decision is now a pure ScrollReadGate, unit-tested; the flaky giant-window headless test is gone. - PrerequisiteRowViewModel no longer inherits ViewModelBase, so the ViewLocator never tries to resolve a view for it. - InstallCompletion completes after the screen moves (TaskCompletionSource); the raw log is written off the UI thread. 146 tests green (35 installer).
…l (Phase 4) PackageDeployer is now transactional on all platforms: stage the whole tree beside the target, move an existing Optimum install to a backup, swap with one rename, delete the backup, roll back to the previous install on any failure. A FailAtStep hook drives the rollback tests. ShortcutWriter writes menu and desktop shortcuts per platform (Linux .desktop plus a hicolor icon, Windows .lnk via WScript.Shell, macOS a symlink into ~/Applications), records them in the manifest, and removes them on uninstall. UninstallRegistration writes and removes the Windows Optimum_is1 uninstall key. RuntimeValidator takes section 7 option 2: a MetadataLoadContext over VintagestoryLib.dll confirms Vintagestory.Client.ClientProgram still has a static Main, no game code executed. InstallManifest gains shortcuts and uninstallRegistryKey. 151 tests green (103 Core).
Adversarial review of the transactional deploy surfaced: - Rollback failure was swallowed and still reported 'rolled back'. The deploy now tracks a committed flag: a pre-swap failure rolls back, a post-swap failure keeps the complete install and downgrades the cleanup error to a warning, and a rollback that cannot restore the backup is reported with the backup's path. - A failure between the swap and the backup delete no longer routes to a rollback that would destroy the good new install. - RegisterInstall is exception-safe and, if the manifest cannot be updated, removes the shortcuts and registry entry it just wrote so nothing is orphaned. - Uninstaller removes each entry independently and always runs the shortcut, registry, and .optimum cleanup even past a locked file; a manifest with an escaping entry is still refused outright. - RuntimeValidator moves its enumeration inside the try, tolerates a partial GetTypes via ReflectionTypeLoadException.Types, and never fails a build it merely could not inspect. - .desktop Exec values are escaped per the Desktop Entry spec. - The macOS link points at a real .app bundle (nested or the install dir when it carries Contents/Info.plist) and is named accordingly. - A cross-filesystem Directory.Move (install dir is a mount point) gives a clear message instead of a misleading 'rolled back'. 157 tests green (106 Core).
Optimum.Installer runs VelopackApp.Build().Run() first in Main and carries an IUpdateService that checks the installer's own GitHub release feed (channel per RID) on startup and shows a non-blocking update banner. It is a no-op outside a Velopack install, so the wizard never waits on it. vpk is a repo-local tool. make installer-pack INSTALLER_RID=<rid> publishes self-contained and packs. release-installer.yml is a manual release that packs win-x64, linux-x64, and osx-arm64, uploads all three, and publishes only Windows and Linux; Windows signing is wired to a secret that is not set yet. ci-installer.yml velopack-smoke now packs the real Optimum.Installer for linux-x64 and checks the AppImage and a delta. Local spike: 48 MB AppImage, 51 KB delta. 157 tests green (38 installer).
Adversarial review of the Velopack work surfaced: - The self-update banner was live on every screen, including a running build; 'Update now' would ApplyUpdatesAndRestart mid-install and orphan the build subprocess. The banner is now gated to Prerequisites and Options (UpdatePromptVisible). - The release matrix had every platform job publish to the same GitHub tag in parallel, racing the API into a half-release. Publishing moved to a single 'publish' job that needs the matrix and uploads one channel at a time. - vpk upload omitted --outputDir, defaulting to a different (empty) directory on the case-sensitive runner. It is passed now. - The Windows signing 'if:' referenced a step-level env var it could not see; the secret is a job-level env var now. - UpdateService: _pending guarded by a lock and snapshot once in ApplyAsync; CheckAsync rethrows a real cancellation instead of swallowing it and narrows the catch. - The banner shows a progress bar while updating and an error line on failure. 158 tests green.
… (Phase 6) - LICENSE-SCOPE.md lists all six installer projects, the test-support project, Optimum.Installer.slnf, and INSTALLER-PLAN.md under MIT. - README.md leads with a graphical-installer section and a command-line section; the per-platform scripts stay below as the original path. - CONTRIBUTING.md documents make installer-test and make installer-pack. - scripts/uninstall.sh is rewritten: it delegates to 'optimum uninstall' for a manifest-based install (which the old script could not touch) and keeps the legacy overlay removal as a fallback. - install-linux.sh and install-windows.ps1 carry a notice pointing at the maintained path but stay functional; the shim swap waits for a green Optimum.Cli build on every platform CI job. 158 tests green.
Clone the matching Optimum source over HTTPS into a per-version user cache when the standalone installer has no repository root. Wire the resolved root through the GUI and expose the same path through the CLI. Use a verified staging checkout and preserve the previous cache until promotion succeeds. Cover cache selection, clone arguments, failure reporting, CLI integration, and the complete wizard handoff. Verification: 186 installer tests pass; Core, CLI, and Installer build with warnings as errors; prerequisite, patch, runtime donor, shader, real shallow clone, and AppImage payload checks pass.
Replaces the EULA modal with a four-step wizard flow (System check, Install options, Review and consent, Install) and a persistent step header, from the ui-ux-pro-max review. Cancel now confirms; the technical log is collapsed by default. Unfinished checkpoint: flow and render tests are green at 49 GUI tests, but the visual pass on the reworked screens and the AppImage rebuild plus use test are still pending. Pushed so the work can continue on another machine.
Completes the checkpoint from ed11c9c: the visual pass and the correctness fixes the reworked flow surfaced. Correctness: - ProgressViewModel no longer disposes its cancellation sources while the Cancel button is still live. A late click after the run finished threw ObjectDisposedException; the 10s forced-cancel escalation faulted every time a cancel succeeded. Both are guarded by a _finished flag and a try/catch, and the sources are disposed only after the Progress view is swapped out. - The update banner hides when dismissed (MainWindow gated the outer Border on UpdatePromptVisible, which ignored Dismissed) and no longer shows on the Review screen, matching the Phase 5 note. - Stepping back to Prerequisites and forward again keeps the Options choices instead of rebuilding the view model. - A second EULA accept keeps the in-flight install task as the awaitable rather than replacing it with a completed one. - The detected data folder is prefilled but not opted into; the default shares Vintage Story's data, which the adjacent copy already says. - The remaining-time estimate waits for a few seconds of real elapsed time before showing, so the first frame is not "about 0:00 left". Strata design system (App.axaml): paper/ink palette with an oxidised-copper accent and moss/clay status colours in both themes; Spectral (SIL OFL, bundled) for step titles, Inter for body, a monospace face for the log; primary/secondary/ghost button tiers on a low corner radius; Fluent checkbox and text-field accents remapped onto the copper. A left step rail shows the four layers with the current one lit and finished ones checked; the content header resolves away on Completion so the rail carries the finished state. Prerequisites folds every ready tool into one summary line. Progress shows an always-visible, auto-scrolling log pane. Completion and the failure state are full-bleed rather than a floating card. Tests: 55 GUI tests (was 49) - added the late-cancel no-op, the mid-run cancel with no unobserved exception, Options choices surviving a step back, the rail step states, and the banner dismiss / not-on-Review cases. The consent ScrollChanged wiring stays a manual check. Verified: 201 tests green (127 Core, 19 CLI, 55 GUI); the packaged linux-x64 AppImage launches and renders with the bundled font and every converter resolved.
…ows-aware The Core layer treated the bash pipeline's tool list and the Linux non-FHS check as universal, so a Windows host was permanently blocked: - NixEnvironment: the NixOS and glibc-interpreter checks now short-circuit on non-Linux, so a leftover NIX_STORE or a resolved C:\lib64\... path can no longer make Windows look non-FHS. This alone reaches the existing (dead) Windows dotnet-install plan in SdkAcquisition. - PrerequisiteScanner: the definition table is now a function of probe.Os. Windows requires only the .NET SDK, Git, and PowerShell -- bootstrap.ps1 reimplements the perl/python3 fixups natively -- and drops the Unix-only rows instead of showing them as missing. Git and PowerShell missing on Windows route to their download pages. - PowerShellHost: new resolver, pwsh with a powershell.exe 5.1 fallback on Windows, used by the scanner row and the build driver. - ScriptBuildDriver: spawns the resolved PowerShell instead of a bare `pwsh`, skips check-patches.sh on Windows (no PowerShell port; the only stock bash is WSL and would run against the wrong filesystem), and catches a spawn failure in RunStep so it fails the build instead of escaping unobserved and hanging the wizard on the Progress screen. - DotnetSdkProbe: split OPTIMUM_DOTNET_CANDIDATES on the simulated platform's separator, not the host's. Adds Windows-shaped test coverage across all four areas.
…pping - Button: add HorizontalContentAlignment/VerticalContentAlignment=Center. The Fluent template left ContentControl's Stretch default, so the single-line label stretched to the padded box and rendered top-aligned, ~1.5-2.5 px above centre on every button. - Button.accent: drop the BorderThickness=0 override. The presenter already paints the border in the accent colour, so appearance is unchanged, but the 0 px border made the accent content box 1 px shorter than the outlined tiers, misaligning adjacent buttons in every footer by 1 px. - TextBlock: move LineHeight=20 off the universal selector onto .muted (the copy that actually wraps). A LineHeight below Spectral SemiBold's ~1.52 em natural line box made Avalonia clip the descender and drop the top leading, so the page heading's "y" painted into the description below it. .display gets an explicit LineHeight of 42; the build-log line gets 16.
SdkAcquisition and IlspycmdAcquisition were planners with no runner, so the GUI could never act on an Automatic prerequisite that was not appimagetool. - AcquisitionProcess: shared CliWrap runner that streams into an IBuildObserver and turns a spawn failure into a negative exit code with a message rather than an exception. - SdkInstaller (ISdkAcquisition): downloads the official dotnet-install script over HTTPS and runs it with the plan's --install-dir/--no-path/ --jsonfile flags, then verifies with DotnetSdkProbe.Find. Never touches PATH. The script fetch is a test seam. - IlspycmdInstaller (IIlspycmdAcquisition): runs `dotnet tool update -g ilspycmd --version <pin> --allow-downgrade` through the discovered dotnet, pin from .config/dotnet-tools.json. - SdkAcquisition: the Windows plan resolves pwsh/powershell.exe instead of a bare `pwsh`.
…list
The Prerequisites screen only had an Install button for appimagetool, and
the row list sat in an Auto grid row with no scroller, so on a bare machine
the Continue button rendered off the bottom of the window -- the install was
unreachable even once every tool was in place.
- PrerequisitesView: the source card, attention rows, and ready summary now
live in a ScrollViewer that takes the middle grid row; the summary header
and the Continue footer stay pinned. A Manual row shows its command inline
in a SelectableTextBlock.
- PrerequisitesViewModel: a row's action is resolved from its
AcquisitionKind -- Automatic dispatches to the SDK / ilspycmd / appimagetool
service by id, DownloadPage opens the browser ("Get it"), Manual/None get
no button. A successful install rescans. InstallerServices.CreateReal now
builds the two new executors; MainWindowViewModel passes them through.
- ProgressViewModel: a catch-all around build-and-deploy routes every
failure to the completion screen so no exception can strand the wizard on
the Progress screen.
- OptionsView: Browse buttons for the install and data folders via
StorageProvider.OpenFolderPickerAsync.
- csproj: drop the Avalonia.Diagnostics reference -- it has no 12.x build, so
a Debug restore failed with NU1102.
Adds a render test that the Continue button stays on screen when the row
list overflows, and Windows-shaped view-model coverage for the SDK install
and the Git download-page actions.
Rendered captures showed two things the earlier alignment pass missed: - Text was drawn with LCD subpixel antialiasing, so every label carried a cyan/orange colour fringe -- very visible on white-on-accent buttons. RenderOptions.TextRenderingMode=Antialias on the window switches the whole tree to grayscale AA. - Line-box centring still left the ink visibly high, because Inter's line box carries descent space the descenderless button labels never use. Button padding goes to 16,10,16,6 (2px more top than bottom) to land the ink on the optical centre.
Replaces the hand-rolled "Strata" design system, which kept fighting the Fluent theme on control metrics (button label centring, text fringing, inconsistent spacing). SukiUI ships tuned control themes, a token system, and the controls a wizard wants. - csproj: SukiUI 7.0.1 in, Avalonia.Themes.Fluent out. SukiUI pulls Avalonia.Themes.Simple, Skia, DataGrid, ColorPicker. - App.axaml: <suki:SukiTheme ThemeColor="Orange"> replaces FluentTheme and the whole Strata ResourceDictionary. One tweak: GlassCard radius 10, no hover animation. - MainWindow is now a SukiWindow (custom chrome, Optimum logo) with a suki:VerticalStepper rail bound to the wizard step, and a SukiTransitioningContentControl for the step content. MainWindowViewModel gains StepIndex / StepLabels for the stepper. - All six views reworked onto GlassCard, SukiUI text classes (h3/h5/Caption/ Primary/Warning/Danger), and the Flat / Basic button classes. The build log collapses into an Expander; the % readout is a plain h2. - InstallerConverters resolves against SukiUI's Suki* colour resources. - RenderOptions.TextRenderingMode=Antialias stays, on the SukiWindow. Strata's Spectral font is removed with it. 212 tests green.
…ype scale Three OFL 1.1 faces ship in Assets/Fonts with their licences copied beside the executable: - Figtree body and controls - Fraunces 72pt page titles and the progress percentage (DisplayFont) - Geist Mono the build log (MonoFont) The default family is set through FontManagerOptions (Segoe UI then Inter as fallbacks); Avalonia.Fonts.Inter and .WithInterFont() are dropped. App.axaml carries an accessibility-minded scale on a 16px base: 16 body / 24 line-height, 14 caption, 13 eyebrow, 18 h5, 28 h3, 34 h2, 15 buttons and inputs at a 40px min height, 13 mono log. Inline sub-14 sizes in the views are lifted to the scale. TextRenderingMode moves to the non-obsolete TextOptions attached property.
The static weight files each carried a different name-table family (ID 1:
"Figtree", "Figtree Medium", "Figtree SemiBold"), so DirectWrite grouped
them into separate families and "avares://...#Figtree" matched nothing on
Windows even though Skia resolved it fine on Linux. Fraunces happened to
work because one of its files was named exactly "Fraunces 72pt".
- One variable file per family: Figtree-VF and GeistMono-VF (single wght
axis), Fraunces flattened from the 72pt SemiBold static.
- fontTools pass forces a consistent family name across name IDs 1/2/4/16/17/
21/22 and a 400 default weight, so Skia and DirectWrite agree.
- Reference by explicit file URI (".../Figtree-VF.ttf#Figtree"), not a folder
URI, and add FontFamily on the SukiWindow as a second anchor.
- Licences copied per family into licences/.
Bumps every size for readability: - body 18 / line-height 29, secondary copy 16, eyebrow 15, section title 21, page title 33 (Fraunces), progress readout 42 - buttons, text boxes, combo boxes and checkboxes at 17px with a 44px minimum height (touch-target guidance) - monospace log 15 - window 980x820 (min 900x720), wider content column and gutters Nothing readable now drops below 15px.
- Body text is Medium weight at full SukiText contrast; the earlier Antialias override is dropped so Windows renders it with ClearType, which reads noticeably less thin. - New .lede class (18px, SukiLowText) for the context line under each heading; .Caption is pinned to #555 rather than the faint control grey. - The "N tools ready: ..." summary renders in Geist Mono, since it is a list of tool and command names.
… buttons The global "Button" style set MinHeight 44 and 20,10 padding, which also hit SukiWindow's minimize/maximize/close buttons -- they stretched vertically and the minimize dash fell out of line. Scope the style to Button:not(.WindowControlsButton); SukiWindow sizes those itself.
Drops the custom App.axaml type scale, button sizing, GlassCard tweaks, the .lede class and the colour overrides. The SukiUI theme now drives sizing, weight, spacing and colour; Figtree / Fraunces / Geist Mono stay, wired through SukiUI's DefaultFontFamily resource plus the Avalonia font default. - Options sections are suki:GroupBox inside suki:GlassCard - the ready-tools summary and the completion outcome are suki:InfoBar (Title + Message; InfoBar has no content presenter, so anything with a button -- the update banner, a prerequisite row, the cancel prompt -- is a GlassCard instead) - the progress percentage is suki:WaveProgress - headings use the h2/h5 classes; Fraunces stays inline on the page title; the build log and tool summary keep Geist Mono - window 960x760 InstallerConverters trimmed to StatusBrush / OutcomeSeverity / LogLevelBrush.
Lexend (a reading-proficiency-tuned OFL face) replaces Figtree and Fraunces for all UI text; JetBrains Mono replaces Geist Mono for the log and command lines. Both are single variable files with clean family names.
InfoBar has no way to set the message font, so the ready-tools card goes back to a GlassCard with a monospace SelectableTextBlock.
SukiUI's 14px base and near-#8a8a8a caption colour were below comfortable reading contrast. Body goes to 15/1.5, headings a step up, and .Caption uses SukiLowText (#555, ~7:1) instead of the faint control grey.
…clip WaveProgress renders from an SKSL shader and fell back to a solid square where the shader was unavailable. suki:CircleProgressBar is a plain Arc, so it draws everywhere; the percentage sits in the middle. Also gives the h2/h3/h5 heading styles their own line height -- the 22px body value from the previous commit was clipping their caps.
It was a bottom-of-form InfoBar whose Title truncates. Now it is a wrapping Danger line directly under the install-location box, announced assertively.
… heading It was a small InfoBar floating in a vertically-centred, left-clamped column with no title, and the InfoBar showed an info glyph for a success. Now it matches every other step: a coloured outcome badge, an h2 headline, the subtext, the install-path card, then the actions.
Both directory boxes relied on placeholder-only labelling, which vanishes on input. Each now has a short label above it, so the data-folder box in particular keeps its context.
Every section was a suki:GroupBox (own header rule) nested in a suki:GlassCard (own border) -- a box in a box. Sections are now a GlassCard with an h5 title, matching SukiUI's own dashboard pattern.
…nstall StepIndex was pinned at 3 on the completion screen, so the rail kept step 4 "current". It now points past the last step when the install succeeded.
When Windows has client-area animations turned off, the wizard swaps its fading SukiTransitioningContentControl for a plain ContentControl so step changes are instant.
…psed strip The log was a collapsed Expander with a large empty gap beneath it. It is now a card that fills the remaining space, so build output is visible as it streams.
SukiUI's VerticalStepper reveals a description slot on the active item, which reflows its label vertically every time the step changes. The rail is now a plain ItemsControl over the wizard steps with fixed 48px rows: a filled accent circle (or a check) for reached steps, a hollow outline for pending ones, and the label in a fixed position.
Replaces the stock SukiUI orange with a SukiColorTheme built from the logo: the teal gem (#009E7F) as the primary that buttons, the step rail and links run on, and the gear gold (#C8A84B) as the accent. The window icon and the title-bar logo are now the actual Optimum mark.
… came on ilspycmd, git and dotnet write progress and advisories to stderr, so the log painted every "not using the latest version" line red. InstallerLogFilter gains Classify(line, fromStdErr): a real failure is error, a version / "warning" advisory is warn (yellow), the rest is info.
…rast Both long-text areas blended into their card. They now sit on a dedicated InsetSurface brush (a step darker in dark mode, a step lighter... darker grey in light) with a top hairline, so they read as a distinct document region. GlassCard corner radius also drops to 12 -- at 20 the rounding was clipping the last line of a card whose padding was smaller than the radius.
…field shown Window height to 800, Options card padding and spacing tightened, and the zero-margin headings from the previous commit together let all three sections plus the conditional data-folder field sit in view. Also swaps the SukiUI VerticalStepper leftovers... (window min height raised to match).
…ible log The build log was a wall of red CS0618 "obsolete" and NETSDK1086 lines -- noise from decompiling and recompiling someone else's code, none of it a blocker. The visible pane now shows only real failures (red), the decompiler "you are not using the latest version" advisory (yellow), and progress markers; the rest stays in the saved raw log. Failure and advisory matching is much tighter (no more "0 Error(s)" or a stray "error" word turning a line red).
Clicking "Launch Optimum" now:
- flips the button to a disabled spinner ("Launching Optimum...")
- starts the launcher, then waits for the game to put a window up (polls the
Optimum / Vintagestory process for a main window handle on Windows; a short
fixed wait elsewhere; 45s ceiling either way)
- asks the shell to Shutdown() once the window is there
CompletionViewModel raises ExitRequested; MainWindowViewModel forwards it;
App wires it to desktop.Shutdown().
The phase title, status line and the "runs locally / elapsed" row were split across two grid rows while the ring spanned both, so the text sat with more space above it than below. They are now a single StackPanel centred against the ring in a symmetric-padding card. The elapsed clock uses the monospace face so its width does not jitter each second.
The label column was a star width, so every row stretched to the card edge and left a big empty margin right of the text while the circle sat tight to the left. Each row is now a fixed 104px (circle + widest label) and the list is centred in a narrower rail, so the gap left of the circle and the gap right of the label are about equal.
…lour Bundles Assets/optimum-grain.png -- a 128px tileable film grain with ~6% baked alpha, neutral so it sits over both the light and the dark SukiUI background. A hit-test-invisible Border tiles it behind the whole shell.
…gh them The grain ImageBrush sits behind the whole window. SukiUI GlassCards and InfoBars default to a translucent fill, so the texture showed through every card as well as the background. Force IsOpaque on both so the grain reads only on the window surface and rail gutter, not on the content cards.
…grey SukiUI's opaque GlassCard key is a flat neutral grey (#333333 dark / #fcfcfc light). The Optimum primary tints the window base a warm olive, so neutral cards read as the wrong colour sitting on it. Override SukiGlassCardOpaqueBackground per theme with the colour a translucent GlassCard composites to over the flat surface (#393926 dark, #FAFAFA light) - same hue as the background, one step lifted. Drop the unused suki|InfoBar opaque style.
The teal primary (#009E7F) read as green, and SukiUI derives the window background from it with an algorithm that skews it further. Switch to a deep cyan primary (#0B7C97) + brighter cyan accent (#22A5C2), and stop leaning on SukiUI for the surfaces: - App.axaml sets AppSurfaceBrush, the card fill and the inset panes to explicit cyan-slate tones per theme (all lean blue, B > G, so nothing reads teal or grey). - MainWindow paints an opaque AppSurfaceBrush ground, covering SukiUI's primary-tinted 'Flat' shader background. - optimum-grain.png regenerated with a faint cyan bias. - The completion badge and the 'system tools ready' check now use the cyan primary instead of green; warning/error/info stay semantic.
…ler.<ext>
vpk pack has no name-override option, so after packing rename the
user-facing files (Setup, Portable, AppImage) to the Optimum scheme and
patch assets.<channel>.json so vpk upload still finds them. The .nupkg and
the releases.<channel>.json / RELEASES-<channel> feed files keep their
Velopack names - the updater fetches those by name and users never see
them. 'Installer' in the name keeps these distinct from the ready-to-run
game packages (Optimum-v<version>-<rid>.{zip,AppImage,dmg}).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Lands the Optimum installer stack on
main. None of it is onmaintoday.Optimum.Bootstrap.Core: the engine. Prerequisite scan and acquisition, client download and decompile, patch and build, transactional deploy with rollback, shortcut writing, registered uninstall.Optimum.Cli: theoptimumverb interface over Core, with a consent gate and an NDJSON progress stream.Optimum.Installer: an Avalonia 12 / SukiUI wizard driving the same Core engine. Prerequisite checklist with in-place tool install, install-location and data-folder choices, a live build log, and a completion screen that launches the game and closes the installer.win-x64andlinux-x64.osx-arm64andosx-x64are built for archival, not published.ci-installer.ymlbuilds and tests theOptimum.Installer.slnfscope and runs a Velopack pack smoke test.release-installer.ymlisworkflow_dispatch: it packs, renames the artifacts toOptimum-v<version>-<rid>-Installer.<ext>, and publishes the feed.INSTALLER-PLAN.mdtracks the phase history. The branch also carries the Windows install-path fixes (per-OS prerequisite scan, PowerShell resolution, in-place SDK and ilspycmd executors), the move from the earlier design system to stock SukiUI 7.0.1, bundled Lexend and JetBrains Mono, a cyan theme, and a UX review pass.Testing
dotnet test Optimum.Installer.slnf -c Release: 230 pass (134 Core, 19 CLI, 77 GUI). Thelinux-x64AppImage was built and launched under WSLg. Thewin-x64self-contained build was run on Windows and checked against real DirectWrite font rendering.Not done
Live click-through automation, the consent scroll-tracking render test, and a real per-platform install into a launched game are still open. Nothing is code-signed, so the Windows
Setup.exetrips SmartScreen and the macOS.pkgtrips Gatekeeper.